#!/usr/bin/env python
# -*- coding: utf-8 -*-

import socket
import select
import datetime
from threading import Thread

BUFFER_SIZE = 8192

class Connection(Thread):
    """ 
    Cette classe gère les connections client-proxy et proxy-serveur.
    A chaque fois qu'un client se connecte au proxy, un thread est lancé
    et se charge d'éffectuer la requête demandée. Lorsqu'une nouvelle 
    requête arrive, le résultat de celle-ci est stocké en cache.
    Si dans le futur la demande est renouvelée, alors le proxy interroge
    le serveur afin de savoir si il y a eu modification du résultat depuis. 
    Si oui, alors le proxy transmet la requête au serveur afin d'obtenir 
    le nouveau résultat, le cache est mis à jour et le résultat est transmis
    au client. Dans le cas contraire, le résultat de la requête est directement
    envoyé au client, sans passer par le serveur. Ceci permet d'économiser
    de la bande-passante. 
    """
    
    def __init__(self, sock_client, ip_client, cache):
        """ 
        Constructeur. 
        Initialise le thread et le buffer. 
        Défini le client et le cache utilisé. 
        """

        Thread.__init__(self)
        self.client = sock_client
        self.ip_client = ip_client
        self.buffer = ""
        self.cache = cache
        

    def run(self):
        """ 
        Surcharge de la fonction run de la classe Thread.
        Cette fonction effectue les actions suivantes:
        - Elle se charge d'obtenir la requête du client
        - Elle vérifie si la requête est correct.
        - Elle vérifie si la requête se trouvent deja en cache.
        - Si la requête est en cache, elle vérifie si il y a eu modification.
          - Si il y a eu modification, elle met à jour le cache et envoie
            le nouveau résultat au client.
          - Sinon elle envoie au client le résultat se trouvant en cache.
        - Si la requête n'est pas en cache, elle envoie la requête au
          serveur, ajoute le résultat en cache et l'envoie au client.
        - Finalement, elle ferme la connection avec le client et le serveur.
        """

        self.method, self.hname, self.path, self.prot = self.get_header()
	print "Requête de", self.ip_client, ": \n"
        if self.method in ("OPTIONS", "GET", "HEAD", "POST", "PUT",
                           "DELETE", "TRACE"):
            
            self.connect(self.hname)
            incache, content = self.cache.find(self.method, self.hname,
                                               self.path)
            if incache:
                if self.modified(self.method,self.hname,self.path, 
                                 self.prot, content):
                    self.client.send(self.buffer)
                    self.cache.update(self.method, self.hname, self.path,
                                      self.date, self.buffer)
                    self.buffer = ""
                else:
                    self.client.send(content[4])
            else:
                print "Page pas en cache. \n"
                self.send_request()
            self.client.close()
            self.host.close()
        else:
            self.client.close()
            

    def get_header(self):
        """ 
        Cette fonction extrait la ligne de requête du message envoyé par
        le client. Elle retourne la méthode, le nom de domaine du serveur
        à contacter, le chemin du fichier sur le serveur ainsi que le 
        protocol utilisé.
        """

        while True:
            self.buffer = self.buffer + self.client.recv(BUFFER_SIZE)
            crlf = self.buffer.find('\n')
            if crlf != -1:
                break
        print self.buffer
        header = self.buffer[:crlf+1].split()
        method = header[0]
        tmp = header[1][7:]
        plimit = tmp.find("/")
        hname = tmp[:plimit]
        path = tmp[plimit:]
        prot = header[2]
        self.buffer = self.buffer[crlf+1:]
        return (method,hname,path,prot)


    def connect(self, hostname):
        """
        Cette fonction se charge de connecter le proxy au serveur distant.
        Elle prend en paramètre le nom de domaine de l'hôte à contacter.
        """

        (sock_type,_,_,_,ip_host) = socket.getaddrinfo(self.hname, 80)[0]
        self.host = socket.socket(sock_type)
        self.host.connect(ip_host)
        print "Connexion avec", hostname, "établie. \n"

    def modified(self, method, hname, path, prot, content):
        """
        Cette fonction se charge d'aller vérifier si une page
        a été modifiée depuis la dernière fois qu'elle a été
        consultée. 
        Elle prend en paramètre la méthode, le nom de domaine de l'hôte à 
        contacter, le chemin du fichier dans l'arborescence du serveur, le 
        protocol utilisécet le contenu du cache pour cette requête. 
        Celle-ci retourne True si la page a été modifiée, False sinon.
        """
        buff = self.buffer[:len(self.buffer)-2]
        request = (method + " " + path + " " + prot
                   + "\r\n" + buff + "If-Modified-Since: " + content[3]
                   + "\r\n\r\n")
        self.host.send(request)
        response = ""
        timeout = 20
        i = 0
        while True:
            i = i + 1
            (recv,_,error) = select.select([self.host],[],[self.host],3)
            if error:
                break
            if recv:
                temp = recv[0].recv(BUFFER_SIZE)
                if temp:
                    response = response + temp
                    i = 0
            if i == timeout:
                break
        index = response.find("\n")
        rsp_header = response[:index]
        code = rsp_header.split()[1]
        if code == "304":
	    print "Page", hname + path, "en cache (non modifiée). \n"
            self.buffer = ""
            return False
        else:
	    print "Page", hname + path, "en cache (modifiée). \n"
            print "Nouvelle version: \n"
	    print response 
            self.date = self.get_date(response)
            self.buffer = response
            return True

    def send_request(self):
        """
        Cette fonction se charge d'envoyer la requête au serveur et de
        transmettre le résultat au client.
        """

        request = ('%s %s %s\n'%(self.method, self.path,
                                   self.prot)+self.buffer)
        self.host.send(request)
        self.buffer = ''
        self.get_response()

    def get_response(self):
        """
        Cette fonction se charge de recueillir la réponse du serveur.
        Une fois la réponse reçue, le cache est mis à jour et la réponse
        transmise au client.
        """

        timeout = 20
        i = 0
        data = ""
        while True:
            i = i + 1
            (recv,_,error) = select.select([self.host], [], [self.host],3)
            if error:
                break
            if recv:
                temp = recv[0].recv(BUFFER_SIZE)
                if temp:
                    data = data + temp
                    self.client.send(temp)
                    i = 0
            if i == timeout:
                break
        print "Réponse à", self.method, self.hname + self.path, self.prot, "\n"
        print data
        self.date = self.get_date(data)
        self.cache.add(self.method, self.hname, self.path, self.date, data)

    def get_date(self, header):
        i = header.find("\r\n\r\n")
        tab = header[:i].split("\r\n")[1:]
        for line in tab:
            sline = line.split(": ")
            if sline[0] == "Last-Modified":
                return sline[1]
        now = datetime.datetime.now()
        return now.strftime("%a, %d %b %Y %H:%M:%S GMT")
